﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Reflection;
using Beaker.Injection;

namespace OpenCube.Nbt.Mapping.Converters
{
	internal sealed class BuildInConverterFactory
		: IBuildInConverterFactory
	{
		private readonly IQueryContainer _container;
		private readonly IDictionary<Type, TypeMap> _typeMaps;

		public BuildInConverterFactory(IQueryContainer container)
		{
			Contract.Requires<ArgumentNullException>(container != null);
			_container = container;

			_typeMaps = new Dictionary<Type, TypeMap>();

			MapConverter<bool, sbyte, BooleanConverter>();
			MapConverter<DateTime, long, DateTimeUnixLongConverter>();
		}

		private void MapConverter<TModel, TNbt, TConverter>()
			where TConverter : class, INbtTypeConverter
		{
			TConverter converter = _container.GetInstance<TConverter>();

			if (!converter.CanConvertToModel(new[] { typeof(TNbt) }, typeof(TModel)) || !converter.CanConvertToNbt(typeof(TModel)))
				throw new InvalidOperationException("Cannot add " + typeof(TConverter).Name + " as the default converter between " + typeof(TModel).Name + " and " + typeof(TNbt));

			_typeMaps.Add(
				typeof(TModel),
				new TypeMap(typeof(TNbt), typeof(TConverter))
			);
		}

		public INbtTypeConverter Create(Type modelType)
		{
			// try to get a build-in converter
			TypeMap map;
			if (_typeMaps.TryGetValue(modelType, out map))
				return (INbtTypeConverter)_container.GetInstance(map.ConverterType);

			// when not a build in, but of type class that is not string
			// then a sub-class
			if (modelType != typeof(string) && modelType.GetTypeInfo().IsClass)
			{
				Type nestedConverterType = typeof(NestedConverter<>).MakeGenericType(modelType);
				return (INbtTypeConverter)_container.GetInstance(nestedConverterType);
			}

			// when none found, create the passthrough converter
			Type passthroughConverterType = typeof(PassthroughConverter<>).MakeGenericType(modelType);
			return (INbtTypeConverter)_container.GetInstance(passthroughConverterType);
		}

		private struct TypeMap
		{
			private readonly Type _nbtType;
			private readonly Type _converterType;

			public TypeMap(Type nbtType, Type converterType)
				: this()
			{
				_nbtType = nbtType;
				_converterType = converterType;
			}

			public Type NbtType
			{
				get { return _nbtType; }
			}

			public Type ConverterType
			{
				get { return _converterType; }
			}
		}
	}
}
